/* * Copyright (c) 2016 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.core.service.impl.hibernate; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import org.hibernate.Criteria; import org.hibernate.EntityMode; import org.hibernate.HibernateException; import org.hibernate.annotations.common.util.StringHelper; import org.hibernate.criterion.CriteriaQuery; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Restrictions; import org.hibernate.criterion.SimpleExpression; import org.hibernate.engine.spi.TypedValue; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; /** * A copy of Hibernate's Example class, with modifications that allow you to * include many-to-one and one-to-one associations in Query By Example (QBE) * queries. * * @author original code by Gavin King, modified by jkelly * @see <a href="http://forum.hibernate.org/viewtopic.php?t=942872">Example and association class </a> */ @SuppressWarnings("UnusedDeclaration") public class AssociationExample implements Criterion { private static final long serialVersionUID = 1545101873498985131L; private final Object entity; private final Collection<String> excludedProperties = new HashSet<String>(); private PropertySelector selector; private boolean isLikeEnabled; private boolean isIgnoreCaseEnabled; private MatchMode matchMode; private boolean includeAssociations = true; /** * A strategy for choosing property values for inclusion in the query * criteria */ public interface PropertySelector { boolean include(Object propertyValue, String propertyName, Type type); } private static final PropertySelector NOT_NULL = new NotNullPropertySelector(); private static final PropertySelector ALL = new AllPropertySelector(); private static final PropertySelector NOT_NULL_OR_ZERO = new NotNullOrZeroPropertySelector(); static final class AllPropertySelector implements PropertySelector, Serializable { private static final long serialVersionUID = -4563517949036036320L; @Override public boolean include(Object object, String propertyName, Type type) { return true; } } static final class NotNullPropertySelector implements PropertySelector, Serializable { private static final long serialVersionUID = -1409817364720711277L; @Override public boolean include(Object object, String propertyName, Type type) { return object != null; } } static final class NotNullOrZeroPropertySelector implements PropertySelector, Serializable { private static final long serialVersionUID = -2071586786806088292L; @Override public boolean include(Object object, String propertyName, Type type) { return object != null && (!(object instanceof Number) || ((Number) object).longValue() != 0); } } /** * Set the property selector */ public AssociationExample setPropertySelector( @SuppressWarnings("ParameterHidesMemberVariable") PropertySelector selector) { this.selector = selector; return this; } /** * Exclude zero-valued properties */ public AssociationExample excludeZeroes() { setPropertySelector(NOT_NULL_OR_ZERO); return this; } /** * Don't exclude null or zero-valued properties */ public AssociationExample excludeNone() { setPropertySelector(ALL); return this; } /** * Use the "like" operator for all string-valued properties */ public AssociationExample enableLike(@SuppressWarnings("ParameterHidesMemberVariable") MatchMode matchMode) { isLikeEnabled = true; this.matchMode = matchMode; return this; } /** * Use the "like" operator for all string-valued properties */ public AssociationExample enableLike() { return enableLike(MatchMode.EXACT); } /** * Ignore case for all string-valued properties */ public AssociationExample ignoreCase() { isIgnoreCaseEnabled = true; return this; } /** * Exclude a particular named property */ public AssociationExample excludeProperty(String name) { excludedProperties.add(name); return this; } /** * Create a new instance, which includes all non-null properties * by default * * @param entity * @return a new instance of <tt>Example</tt> */ public static AssociationExample create(Object entity) { if(entity == null) throw new NullPointerException("null AssociationExample"); return new AssociationExample(entity, NOT_NULL); } protected AssociationExample(Object entity, PropertySelector selector) { this.entity = entity; this.selector = selector; } public String toString() { return "example (" + entity + ')'; } private boolean isPropertyIncluded(Object value, String name, Type type) { return !excludedProperties.contains(name) && selector.include(value, name, type) && (!type.isAssociationType() || type.isAssociationType() && includeAssociations && !type.isCollectionType()); } @Override public String toSqlString(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { StringBuffer buf = new StringBuffer("("); EntityPersister meta = criteriaQuery.getFactory().getEntityPersister(criteriaQuery.getEntityName(criteria)); String[] propertyNames = meta.getPropertyNames(); Type[] propertyTypes = meta.getPropertyTypes(); //TODO: get all properties, not just the fetched ones! Object[] propertyValues = meta.getPropertyValues(entity); for(int i = 0; i < propertyNames.length; i++) { Object propertyValue = propertyValues[i]; String propertyName = propertyNames[i]; boolean isPropertyIncluded = i != meta.getVersionProperty() && isPropertyIncluded(propertyValue, propertyName, propertyTypes[i]); if(isPropertyIncluded) { if(propertyTypes[i].isComponentType()) { appendComponentCondition(propertyName, propertyValue, (CompositeType) propertyTypes[i], criteria, criteriaQuery, buf); } else { appendPropertyCondition(propertyName, propertyValue, criteria, criteriaQuery, buf); } } } if(buf.length() == 1) buf.append("1=1"); //yuck! return buf.append(')').toString(); } private static final Object[] TYPED_VALUES = new TypedValue[0]; @Override public TypedValue[] getTypedValues(Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { EntityPersister meta = criteriaQuery.getFactory().getEntityPersister(criteriaQuery.getEntityName(criteria)); String[] propertyNames = meta.getPropertyNames(); Type[] propertyTypes = meta.getPropertyTypes(); //TODO: get all properties, not just the fetched ones! Object[] values = meta.getPropertyValues(entity); List<TypedValue> list = new ArrayList<TypedValue>(); for(int i = 0; i < propertyNames.length; i++) { Object value = values[i]; Type type = propertyTypes[i]; String name = propertyNames[i]; boolean isPropertyIncluded = i != meta.getVersionProperty() && isPropertyIncluded(value, name, type); if(isPropertyIncluded) { if(propertyTypes[i].isComponentType()) { addComponentTypedValues(name, value, (CompositeType) type, list, criteria, criteriaQuery); } else { addPropertyTypedValue(value, type, list); } } } return list.toArray(new TypedValue[list.size()]); } private EntityMode getEntityMode(Criteria criteria, CriteriaQuery criteriaQuery) { EntityPersister meta = criteriaQuery.getFactory().getEntityPersister(criteriaQuery.getEntityName(criteria)); EntityMode entityMode = meta.getEntityMode(); if(entityMode == null) { throw new ClassCastException(entity.getClass().getName()); } return entityMode; } protected void addPropertyTypedValue(Object value, Type type, Collection<TypedValue> list) { if(value != null) { if(value instanceof String) { String string = (String) value; if(isIgnoreCaseEnabled) string = string.toLowerCase(); if(isLikeEnabled) string = matchMode.toMatchString(string); list.add(new TypedValue(type, string, null)); } else { list.add(new TypedValue(type, value, null)); } } } protected void addComponentTypedValues(String path, Object component, CompositeType type, List<TypedValue> list, Criteria criteria, CriteriaQuery criteriaQuery) throws HibernateException { if(component != null) { String[] propertyNames = type.getPropertyNames(); Type[] subtypes = type.getSubtypes(); Object[] values = type.getPropertyValues(component, getEntityMode(criteria, criteriaQuery)); for(int i = 0; i < propertyNames.length; i++) { Object value = values[i]; Type subtype = subtypes[i]; String subPath = StringHelper.qualify(path, propertyNames[i]); if(isPropertyIncluded(value, subPath, subtype)) { if(subtype.isComponentType()) { addComponentTypedValues(subPath, value, (CompositeType) subtype, list, criteria, criteriaQuery); } else { addPropertyTypedValue(value, subtype, list); } } } } } protected void appendPropertyCondition(String propertyName, Object propertyValue, Criteria criteria, CriteriaQuery cq, StringBuffer buf) throws HibernateException { Criterion criterion; if(propertyValue != null) { boolean isString = propertyValue instanceof String; SimpleExpression se = isLikeEnabled && isString ? Restrictions.like(propertyName, propertyValue) : Restrictions.eq(propertyName, propertyValue); criterion = isIgnoreCaseEnabled && isString ? se.ignoreCase() : se; } else { criterion = Restrictions.isNull(propertyName); } String critCondition = criterion.toSqlString(criteria, cq); if(buf.length() > 1 && critCondition.trim().length() > 0) buf.append(" and "); buf.append(critCondition); } protected void appendComponentCondition(String path, Object component, CompositeType type, Criteria criteria, CriteriaQuery criteriaQuery, StringBuffer buf) throws HibernateException { if(component != null) { String[] propertyNames = type.getPropertyNames(); Object[] values = type.getPropertyValues(component, getEntityMode(criteria, criteriaQuery)); Type[] subtypes = type.getSubtypes(); for(int i = 0; i < propertyNames.length; i++) { String subPath = StringHelper.qualify(path, propertyNames[i]); Object value = values[i]; if(isPropertyIncluded(value, subPath, subtypes[i])) { Type subtype = subtypes[i]; if(subtype.isComponentType()) { appendComponentCondition(subPath, value, (CompositeType) subtype, criteria, criteriaQuery, buf); } else { appendPropertyCondition(subPath, value, criteria, criteriaQuery, buf); } } } } } public boolean isIncludeAssociations() { return includeAssociations; } public void setIncludeAssociations(boolean includeAssociations) { this.includeAssociations = includeAssociations; } }